在開始介紹微服務之前,有一個很重要的概念必須要先闡明,那就是分散式交易。
因此這篇文章會分為幾個部分:
但在解釋什麼是分散式交易前,讓我們來回顧一下什麼是交易。
傳統的交易指的是資料庫能夠提供四項保證,也就是俗稱的ACID。
Read Uncommitted
)、讀已提交(Read Committed
)、可重複讀(Repeatable Read
)和可序列化(Serializable
)。可序列化是最嚴格的保證,任何操作都會強制遵循先後順序,因此不會碰到競爭條件(racing condition)。在簡單的描述完ACID,讓我們看看有哪些資料庫支援。我挑選了三個常見的目標,分別是MySQL
、Mongo
和Redis
。
Write Concern
和Read Concern
就能保證交易的一致性。我認為,MongoDB非常適合用在非正規化的使用場景。MULTI
一次執行很多命令或者透過EVAL
呼叫一個Lua
腳本。看起來Redis可以支援一次大量更新。更有甚者,Redis可以透過AOF或RDB的機制持久化存儲。但我必須說,Redis僅支援交易的部分保證,而不是完整的交易系統。讓我們總結一下。
MySQL | Mongo | Redis | |
---|---|---|---|
Atomicity | V | V | X |
Consistency | V | Write concern | V |
Isolation | V | Read concern | V |
Durability | V | V | X |
我們已經了解傳統的交易了,現在我們來聊聊分散式交易。
為了簡化問題,我提供一個簡單的定義,那就是,在多個同質性/異質性的資料源上提供ACID保證。
第三個例子在MongoDB的4.2版發布後已經解決。
讓我們快速看一個基本的設計概念。
我們對每一個資料源都使用交易,如果有任何一個失敗,那麼就把其他已提交的資料回滾。因此實作會類似這樣:
問題在於,當我們提交給A後,我們很難在B失敗時將這些資料回滾。
即便我們採用巢狀交易,這個問題依然無解。
根據流程顯示,我們很難回滾C。
在微服務的領域,這樣的問題很常見,因此有很多協定被提出來以解決這個常見的設計問題,例如XA
。
XA
是根據2PC
(two-phase commit、二階段提交)協定設計的,但我不打算深入講二階段提交的細節。我只提供一個簡單的概念解釋。
整個交易系統會有一個協調者,任何要執行分散式交易的人都必須向協調者註冊(第一次提交)。協調者收到註冊後會向所有參與的資料源進行輪詢,若是有任何一個資料源拒絕,那麼協調者就會回滾所有的輪詢。
若很幸運的所有資料源都同意,那麼協調者就會告知客戶端,而客戶端就可以發起正式的提交(第二次提交)。
但這樣的流程有三個問題:
看起來多階段提交協定不太實用,我們應該嘗試另一種比較輕量的方案,稱為最終一致性。
字面上的意思是我們放棄了交易系統提供的強一致性保證來避免實作的開銷,轉而希望資料最終能夠一致。為了達成最終一致性的方案,有兩個重要的關鍵字:
結合這兩個概念,現在我們可以開始設計系統了。
首先,我們將所有的交易都當成事件,當一個客戶端想要執行分散式交易時,就直接將交易事件發送出去。事件處理者收到交易事件後就在多個資料源上進行處理。
當所有任務都正確結束,就可以將交易事件標記成已完成,事件處理者就可以處理下一個任務。若是任務中有任何失敗,那麼就不會標註交易事件,等到下次處理者進行重試。
因為冪等性,所以交易事件可以重試無數次直到成功。儘管如此,我還是建議至少設個重試上限。
讓我們來看一下流程圖。
完美,現在我們可以正確處理分散式交易了,所有的資料最終都會保持一致。
雖然在流程圖中有一個訊息佇列(Queue),事實上,不一定需要一個真正的訊息佇列系統。有很多替代方案,例如將所有事件存在資料庫中,為每個事件建立一個狀態欄位,讓處理者定期從資料庫拿。可以根據需求選擇自己偏好的方式。